遊戲中總有某些音效需要循環播放,像是迴旋鏢在飛行時產生的咻咻聲,小火堆的辟啪聲,牛群經過時的咚咚聲。如果遊戲能確保聲音來源的數量,那可能沒什麼問題。但像是Minecraft或是其他支援玩家自製模組的遊戲,那麼一次上來三百個咻咻聲,或是上千隻牛迎面走來咚咚咚,別說對遊戲效能的影響了,就是人耳可能第一個就受不了。
我們需要一個方法管理遊戲中數百個環境音,將這些環境音動態地濃縮成適當的數量,又不失原味。
這個系統的邏輯很簡單,每個想要發聲的音源(如迴旋鏢)都要在中控系統中登記想要發聲的音量大小,然後統一由中控系統來發聲。
/** 寫一個音源的類別,包含音源ID和音量
* 這個類別不是遊戲中的物件,
* 而是中控系統內部用來識別音源用的物件代理。
*/
class SoundSource {
// 建構子
constructor(
public id: string, // 音源ID
public volume: number // 音量
) { }
}
假設我們定義中控系統最多能同時播放三個聲道,那麼在每一幀的更新函式裏,中控系統會選出最大聲的三個聲源,並以這三個聲源登記的音量去調整正在發聲的聲道。
/** 環境音中控系統 */
class SoundCentralController {
// 聲道的陣列
channels: PIXI.sound.IMediaInstance[] = [];
// 音源Map
sourceMap: { [key: string]: SoundSource } = {};
// 建構子,給一個最大聲道數量的參數
constructor(public maxChannels: number) {
}
// 登記音源函式(音源ID,音量)
registerSoundSource(id: string, volume: number): void {
// 若登記時,音量為正
if (volume > 0) {
// 從soundMap取出這個id的音源(有可能是空的)
let source = this.sourceMap[id];
if (!source) {
// 若原本沒有這個音源,就建立一個新的
this.sourceMap[id] = source = new SoundSource(id, volume);
} else {
// 否則只要調整音源的音量就行
source.volume = volume;
}
} else if (this.sourceMap[id]) {
// 登記音量為零的時候,表示可以刪掉這個音源
// delete 是刪除物件某個屬性的關鍵字
delete this.sourceMap[id];
}
}
// 每幀更新函式(其實每10幀更新一次也行,看遊戲對這個音效的更新頻率需求)
update(): void {
// 用Object.values()把this.sourceMap裏的東西拿出來變成一個陣列
let sources: SoundSource[] = Object.values(this.sourceMap);
// 利用ArrayUtil提供的排序工具,將sources依volume屬性
// 從大排到小(最後一個參數)
ArrayUtil.sortNumericOn(sources, 'volume', false);
// 將排序好的音源,取最前面的幾個(maxChannels個)
sources = sources.slice(0, this.maxChannels);
// 放一個迴圈,跑過每個音源
for (let i = 0; i < sources.length; i++) {
let source = sources[i];
// 從目前的聲道找出對應i的聲道
let channel = this.channels[i];
if (!channel) {
// 如果目前聲道沒那麼多,那就新建一個播放聲道
// 並設定循環播放(loop)
this.channels[i]
= channel
= playSound("ironman2022_trick24.shu", { loop: true });
}
// 依音源更新聲道的音量
channel.volume = source.volume;
}
// 放一個迴圈,跑過每個沒用到的聲道
for (let i = sources.length; i < this.channels.length; i++) {
// 將沒用到的聲道滅了
this.channels[i].destroy();
}
// 只取有用的聲道,跟後面沒用到的說byebye。
// 直接把陣列的長度改短,
// 是最快可以把長度後面的元素刪掉的方法。
this.channels.length = sources.length;
}
}
有了上面寫好的中控系統,就可以寫個小範例來測試效果了。
CG示範程式
示範程式會放一個主角在畫面中間,然後丟出數十把迴旋鏢,迴旋鏢發出的音量與主角的距離成反比。主角外圍的圓圈是主角可以聽到聲音的範圍。
在示範程式中可以實際體驗這個系統帶來的好處,即使同時有數十把迴旋鏢進入聽力範圍,系統仍然只會選出三個最有影響力的聲源來發聲,不但維護了遊戲的效能,而且也沒有失去鏢聲雜亂的效果。
同學們還可以更進一步,將這個系統改成能夠支援立體音的播放,把左右聲道分開管理,這應該會是個非常有趣的練習題目。